package com.yuncheng.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

/**
 * SignUtil
 *
 * @date 2023/5/10
 */
public class SignUtil {

    private static final Logger logger = LoggerFactory.getLogger(SignUtil.class);

    // code参数key
    public static final String CODE_KEY = "_code_";
    // appid参数key
    public static final String APPID_KEY = "_appid_";

    // 时间戳参数key
    public static final String TIMESTAMP_KEY = "_timestamp_";

    // 防重放攻击参数key
    public static final String NONCE_KEY = "_nonce_";

    // 参数签名结果key
    public static final String SIGN_KEY = "_mosign_";

    // 默认时间戳差值超时的阈值，单位为毫秒，默认为10分钟
    public static final long DEFAULT_TIMESTAMP_TIMEOUT_MS = 1000L * 60 * 10;

    // 生成nonce参数值的随机数变量
    private static final Random nonceRandom = new Random(System.currentTimeMillis());

    /**
     * 用于建立十六进制字符的输出的小写字符数组
     */
    private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    /**
     * 将字节数组转换为十六进制字符数组
     *
     * @param data byte[]
     * @return 十六进制字符串
     */
    protected static String encodeHexString(byte[] data) {
        if (null == data || 0 == data.length) {
            return null;
        }
        int l = data.length;
        char[] out = new char[l << 1];
        for (int i = 0, j = 0; i < l; i++) {
            out[j++] = DIGITS_LOWER[(0xF0 & data[i]) >>> 4];
            out[j++] = DIGITS_LOWER[0x0F & data[i]];
        }
        return new String(out);
    }

    public static String md5(String source) {
        String md5Result = null;
        try {
            byte[] hash = source.getBytes(Charset.forName("UTF8"));
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.update(hash);
            hash = messageDigest.digest();
            md5Result = encodeHexString(hash);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return md5Result;
    }

    /**
     * 移动端平台生成签名
     * @param code 随机字符串，从移动端平台获取
     * @param appId 应用ID
     * @param appSecret 应用秘钥
     * @return
     */
    public static Map<String, String> genCodeSign(String code, String appId, String appSecret) {
        Map<String, String> params = new HashMap<>();
        params.put(CODE_KEY,code);
        return genSign(params,appId,appSecret);
    }

    /**
     * 生成参数签名
     *
     * @param params    待签名的参数
     * @param appId     签名应用的识别id
     * @param appSecret 签名应用的签名密匙
     * @return 参数签名后的结果，包括待签名的参数、appid参数、时间戳参数、防重放攻击参数、签名结果参数
     */
    public static Map<String, String> genSign(Map<String, String> params, String appId, String appSecret) {
        //使用TreeMap，保证所有参数按key排序
        TreeMap<String, String> result = new TreeMap<>();
        //复制待签名参数
        result.putAll(params);

        //生成签名相关参数
        result.put(APPID_KEY, appId);
        result.put(TIMESTAMP_KEY, Long.toString(System.currentTimeMillis()));
        result.put(NONCE_KEY, Integer.toString(Math.abs(nonceRandom.nextInt())));

        //保证待签名参数中没有签名结果key
        result.remove(SIGN_KEY);

        // 生成待签名字符串，拼接结构为：appSecretKey1Value1Key2Value2...appSecret
        StringBuilder str = new StringBuilder();
        // 1 首部拼接秘钥
        str.append(appSecret);
        //result是TreeMap,keySet已按key排序
        for (String key : result.keySet()) {
            // 2.1 拼接key
            str.append(key);
            String value = result.get(key);
            if (null != value) {
                // 2.2 拼接value
                str.append(value);
            }
        }
        // 3 尾部拼接秘钥
        str.append(appSecret);

        // 4 MD5加密生成参数签名结果
        result.put(SIGN_KEY, md5(str.toString()));
        return result;
    }

    /**
     * 校验时间戳是否超时
     *
     * @param timestamp        待校验的时间戳字符串
     * @param timestampTimeout 校验时间戳差值是否超时的阈值，时间差的绝对值大于阈值为超时，单位为毫秒
     * @return 时间戳未超时返回true，否则返回false
     */
    public static boolean validateTimestamp(String timestamp, long timestampTimeout) {
        if (StringUtils.hasText(timestamp)) {
            try {
                long t = Long.parseLong(timestamp, 10);
                //兼容时间戳以秒为单位的情况
                if (timestamp.length() < 11) {
                    //乘以1000转换为以毫秒为单位
                    t = t * 1000;
                }
                return Math.abs(System.currentTimeMillis() - t) < timestampTimeout;
            } catch (Throwable e) {
                logger.error("error in validateTimestamp", e);
            }
        }
        return false;
    }

    /**
     * 校验参数签名
     *
     * @param params    待待校验的参数
     * @param appSecret 签名应用的签名密匙
     * @return 签名校验有效返回true，否则返回false
     */
    public static boolean validateSign(Map<String, String> params, String appSecret) {
        if (!StringUtils.hasText(appSecret)) {
            // appSecret为空，直接返回校验失败
            return false;
        }
        //使用TreeMap，保证所有参数按key排序
        TreeMap<String, String> result = new TreeMap<>();
        //复制待校验参数
        result.putAll(params);

        //保证待校验参数中没有签名结果key
        result.remove(SIGN_KEY);

        // 生成待签名字符串，拼接结构为：appSecretKey1Value1Key2Value2...appSecret
        StringBuilder str = new StringBuilder();
        // 1 首部拼接秘钥
        str.append(appSecret);
        //result是TreeMap,keySet已按key排序
        for (String key : result.keySet()) {
            // 2.1 拼接key
            str.append(key);
            String value = result.get(key);
            if (null != value) {
                // 2.2 拼接value
                str.append(value);
            }
        }
        // 3 尾部拼接秘钥
        str.append(appSecret);

        // 4 MD5加密生成参数签名结果
        String md5Str = md5(str.toString());

        return md5Str.equalsIgnoreCase(params.get(SIGN_KEY));
    }

    /**
     * 校验参数签名和时间戳
     *
     * @param params           待待校验的参数
     * @param appSecret        签名应用的签名密匙
     * @param timestampTimeout 校验时间戳差值是否超时的阈值，时间差的绝对值大于阈值为超时，单位为毫秒
     * @return 签名校验有效前时间戳不超时返回true，否则返回false
     */
    public static boolean validateSign(Map<String, String> params, String appSecret, long timestampTimeout) {
        if (validateTimestamp(params.get(TIMESTAMP_KEY), timestampTimeout)) {
            return validateSign(params, appSecret);
        }
        return false;
    }

}
